/*
 * Copyright (c) 1999, 2014, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.awt;

import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.peer.RobotPeer;
import java.lang.reflect.InvocationTargetException;
import sun.awt.ComponentFactory;
import sun.awt.SunToolkit;
import sun.awt.image.SunWritableRaster;
import sun.security.util.SecurityConstants;

/**
 * This class is used to generate native system input events
 * for the purposes of test automation, self-running demos, and
 * other applications where control of the mouse and keyboard
 * is needed. The primary purpose of Robot is to facilitate
 * automated testing of Java platform implementations.
 * <p>
 * Using the class to generate input events differs from posting
 * events to the AWT event queue or AWT components in that the
 * events are generated in the platform's native input
 * queue. For example, <code>Robot.mouseMove</code> will actually move
 * the mouse cursor instead of just generating mouse move events.
 * <p>
 * Note that some platforms require special privileges or extensions
 * to access low-level input control. If the current platform configuration
 * does not allow input control, an <code>AWTException</code> will be thrown
 * when trying to construct Robot objects. For example, X-Window systems
 * will throw the exception if the XTEST 2.2 standard extension is not supported
 * (or not enabled) by the X server.
 * <p>
 * Applications that use Robot for purposes other than self-testing should
 * handle these error conditions gracefully.
 *
 * @author Robi Khan
 * @since 1.3
 */
public class Robot {

  private static final int MAX_DELAY = 60000;
  private RobotPeer peer;
  private boolean isAutoWaitForIdle = false;
  private int autoDelay = 0;
  private static int LEGAL_BUTTON_MASK = 0;

  private DirectColorModel screenCapCM = null;

  /**
   * Constructs a Robot object in the coordinate system of the primary screen.
   * <p>
   *
   * @throws AWTException if the platform configuration does not allow low-level input control.
   * This exception is always thrown when GraphicsEnvironment.isHeadless() returns true
   * @throws SecurityException if <code>createRobot</code> permission is not granted
   * @see java.awt.GraphicsEnvironment#isHeadless
   * @see SecurityManager#checkPermission
   * @see AWTPermission
   */
  public Robot() throws AWTException {
    if (GraphicsEnvironment.isHeadless()) {
      throw new AWTException("headless environment");
    }
    init(GraphicsEnvironment.getLocalGraphicsEnvironment()
        .getDefaultScreenDevice());
  }

  /**
   * Creates a Robot for the given screen device. Coordinates passed
   * to Robot method calls like mouseMove and createScreenCapture will
   * be interpreted as being in the same coordinate system as the
   * specified screen. Note that depending on the platform configuration,
   * multiple screens may either:
   * <ul>
   * <li>share the same coordinate system to form a combined virtual screen</li>
   * <li>use different coordinate systems to act as independent screens</li>
   * </ul>
   * This constructor is meant for the latter case.
   * <p>
   * If screen devices are reconfigured such that the coordinate system is
   * affected, the behavior of existing Robot objects is undefined.
   *
   * @param screen A screen GraphicsDevice indicating the coordinate system the Robot will operate
   * in.
   * @throws AWTException if the platform configuration does not allow low-level input control.
   * This exception is always thrown when GraphicsEnvironment.isHeadless() returns true.
   * @throws IllegalArgumentException if <code>screen</code> is not a screen GraphicsDevice.
   * @throws SecurityException if <code>createRobot</code> permission is not granted
   * @see java.awt.GraphicsEnvironment#isHeadless
   * @see GraphicsDevice
   * @see SecurityManager#checkPermission
   * @see AWTPermission
   */
  public Robot(GraphicsDevice screen) throws AWTException {
    checkIsScreenDevice(screen);
    init(screen);
  }

  private void init(GraphicsDevice screen) throws AWTException {
    checkRobotAllowed();
    Toolkit toolkit = Toolkit.getDefaultToolkit();
    if (toolkit instanceof ComponentFactory) {
      peer = ((ComponentFactory) toolkit).createRobot(this, screen);
      disposer = new RobotDisposer(peer);
      sun.java2d.Disposer.addRecord(anchor, disposer);
    }
    initLegalButtonMask();
  }

  private static synchronized void initLegalButtonMask() {
    if (LEGAL_BUTTON_MASK != 0) {
      return;
    }

    int tmpMask = 0;
    if (Toolkit.getDefaultToolkit().areExtraMouseButtonsEnabled()) {
      if (Toolkit.getDefaultToolkit() instanceof SunToolkit) {
        final int buttonsNumber = ((SunToolkit) (Toolkit.getDefaultToolkit())).getNumberOfButtons();
        for (int i = 0; i < buttonsNumber; i++) {
          tmpMask |= InputEvent.getMaskForButton(i + 1);
        }
      }
    }
    tmpMask |= InputEvent.BUTTON1_MASK |
        InputEvent.BUTTON2_MASK |
        InputEvent.BUTTON3_MASK |
        InputEvent.BUTTON1_DOWN_MASK |
        InputEvent.BUTTON2_DOWN_MASK |
        InputEvent.BUTTON3_DOWN_MASK;
    LEGAL_BUTTON_MASK = tmpMask;
  }

  /* determine if the security policy allows Robot's to be created */
  private void checkRobotAllowed() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
      security.checkPermission(SecurityConstants.AWT.CREATE_ROBOT_PERMISSION);
    }
  }

  /* check if the given device is a screen device */
  private void checkIsScreenDevice(GraphicsDevice device) {
    if (device == null || device.getType() != GraphicsDevice.TYPE_RASTER_SCREEN) {
      throw new IllegalArgumentException("not a valid screen device");
    }
  }

  private transient Object anchor = new Object();

  static class RobotDisposer implements sun.java2d.DisposerRecord {

    private final RobotPeer peer;

    public RobotDisposer(RobotPeer peer) {
      this.peer = peer;
    }

    public void dispose() {
      if (peer != null) {
        peer.dispose();
      }
    }
  }

  private transient RobotDisposer disposer;

  /**
   * Moves mouse pointer to given screen coordinates.
   *
   * @param x X position
   * @param y Y position
   */
  public synchronized void mouseMove(int x, int y) {
    peer.mouseMove(x, y);
    afterEvent();
  }

  /**
   * Presses one or more mouse buttons.  The mouse buttons should
   * be released using the {@link #mouseRelease(int)} method.
   *
   * @param buttons the Button mask; a combination of one or more mouse button masks. <p> It is
   * allowed to use only a combination of valid values as a {@code buttons} parameter. A valid
   * combination consists of {@code InputEvent.BUTTON1_DOWN_MASK}, {@code
   * InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} and values returned by the
   * {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method.
   *
   * The valid combination also depends on a {@link Toolkit#areExtraMouseButtonsEnabled()
   * Toolkit.areExtraMouseButtonsEnabled()} value as follows: <ul> <li> If support for extended
   * mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java then it is
   * allowed to use only the following standard button masks: {@code InputEvent.BUTTON1_DOWN_MASK},
   * {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK}. <li> If support for
   * extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java then it
   * is allowed to use the standard button masks and masks for existing extended mouse buttons, if
   * the mouse has more then three buttons. In that way, it is allowed to use the button masks
   * corresponding to the buttons in the range from 1 to {@link java.awt.MouseInfo#getNumberOfButtons()
   * MouseInfo.getNumberOfButtons()}. <br> It is recommended to use the {@link
   * InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method to obtain the mask
   * for any mouse button by its number. </ul> <p> The following standard button masks are also
   * accepted: <ul> <li>{@code InputEvent.BUTTON1_MASK} <li>{@code InputEvent.BUTTON2_MASK}
   * <li>{@code InputEvent.BUTTON3_MASK} </ul> However, it is recommended to use {@code
   * InputEvent.BUTTON1_DOWN_MASK}, {@code InputEvent.BUTTON2_DOWN_MASK},  {@code
   * InputEvent.BUTTON3_DOWN_MASK} instead. Either extended {@code _DOWN_MASK} or old {@code _MASK}
   * values should be used, but both those models should not be mixed.
   * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse
   * button and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled()
   * disabled} by Java
   * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse
   * button that does not exist on the mouse and support for extended mouse buttons is {@link
   * Toolkit#areExtraMouseButtonsEnabled() enabled} by Java
   * @see #mouseRelease(int)
   * @see InputEvent#getMaskForButton(int)
   * @see Toolkit#areExtraMouseButtonsEnabled()
   * @see java.awt.MouseInfo#getNumberOfButtons()
   * @see java.awt.event.MouseEvent
   */
  public synchronized void mousePress(int buttons) {
    checkButtonsArgument(buttons);
    peer.mousePress(buttons);
    afterEvent();
  }

  /**
   * Releases one or more mouse buttons.
   *
   * @param buttons the Button mask; a combination of one or more mouse button masks. <p> It is
   * allowed to use only a combination of valid values as a {@code buttons} parameter. A valid
   * combination consists of {@code InputEvent.BUTTON1_DOWN_MASK}, {@code
   * InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK} and values returned by the
   * {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method.
   *
   * The valid combination also depends on a {@link Toolkit#areExtraMouseButtonsEnabled()
   * Toolkit.areExtraMouseButtonsEnabled()} value as follows: <ul> <li> If the support for extended
   * mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java then it is
   * allowed to use only the following standard button masks: {@code InputEvent.BUTTON1_DOWN_MASK},
   * {@code InputEvent.BUTTON2_DOWN_MASK}, {@code InputEvent.BUTTON3_DOWN_MASK}. <li> If the support
   * for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java
   * then it is allowed to use the standard button masks and masks for existing extended mouse
   * buttons, if the mouse has more then three buttons. In that way, it is allowed to use the button
   * masks corresponding to the buttons in the range from 1 to {@link
   * java.awt.MouseInfo#getNumberOfButtons() MouseInfo.getNumberOfButtons()}. <br> It is recommended
   * to use the {@link InputEvent#getMaskForButton(int) InputEvent.getMaskForButton(button)} method
   * to obtain the mask for any mouse button by its number. </ul> <p> The following standard button
   * masks are also accepted: <ul> <li>{@code InputEvent.BUTTON1_MASK} <li>{@code
   * InputEvent.BUTTON2_MASK} <li>{@code InputEvent.BUTTON3_MASK} </ul> However, it is recommended
   * to use {@code InputEvent.BUTTON1_DOWN_MASK}, {@code InputEvent.BUTTON2_DOWN_MASK},  {@code
   * InputEvent.BUTTON3_DOWN_MASK} instead. Either extended {@code _DOWN_MASK} or old {@code _MASK}
   * values should be used, but both those models should not be mixed.
   * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse
   * button and support for extended mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled()
   * disabled} by Java
   * @throws IllegalArgumentException if the {@code buttons} mask contains the mask for extra mouse
   * button that does not exist on the mouse and support for extended mouse buttons is {@link
   * Toolkit#areExtraMouseButtonsEnabled() enabled} by Java
   * @see #mousePress(int)
   * @see InputEvent#getMaskForButton(int)
   * @see Toolkit#areExtraMouseButtonsEnabled()
   * @see java.awt.MouseInfo#getNumberOfButtons()
   * @see java.awt.event.MouseEvent
   */
  public synchronized void mouseRelease(int buttons) {
    checkButtonsArgument(buttons);
    peer.mouseRelease(buttons);
    afterEvent();
  }

  private void checkButtonsArgument(int buttons) {
    if ((buttons | LEGAL_BUTTON_MASK) != LEGAL_BUTTON_MASK) {
      throw new IllegalArgumentException("Invalid combination of button flags");
    }
  }

  /**
   * Rotates the scroll wheel on wheel-equipped mice.
   *
   * @param wheelAmt number of "notches" to move the mouse wheel Negative values indicate movement
   * up/away from the user, positive values indicate movement down/towards the user.
   * @since 1.4
   */
  public synchronized void mouseWheel(int wheelAmt) {
    peer.mouseWheel(wheelAmt);
    afterEvent();
  }

  /**
   * Presses a given key.  The key should be released using the
   * <code>keyRelease</code> method.
   * <p>
   * Key codes that have more than one physical key associated with them
   * (e.g. <code>KeyEvent.VK_SHIFT</code> could mean either the
   * left or right shift key) will map to the left key.
   *
   * @param keycode Key to press (e.g. <code>KeyEvent.VK_A</code>)
   * @throws IllegalArgumentException if <code>keycode</code> is not a valid key
   * @see #keyRelease(int)
   * @see java.awt.event.KeyEvent
   */
  public synchronized void keyPress(int keycode) {
    checkKeycodeArgument(keycode);
    peer.keyPress(keycode);
    afterEvent();
  }

  /**
   * Releases a given key.
   * <p>
   * Key codes that have more than one physical key associated with them
   * (e.g. <code>KeyEvent.VK_SHIFT</code> could mean either the
   * left or right shift key) will map to the left key.
   *
   * @param keycode Key to release (e.g. <code>KeyEvent.VK_A</code>)
   * @throws IllegalArgumentException if <code>keycode</code> is not a valid key
   * @see #keyPress(int)
   * @see java.awt.event.KeyEvent
   */
  public synchronized void keyRelease(int keycode) {
    checkKeycodeArgument(keycode);
    peer.keyRelease(keycode);
    afterEvent();
  }

  private void checkKeycodeArgument(int keycode) {
    // rather than build a big table or switch statement here, we'll
    // just check that the key isn't VK_UNDEFINED and assume that the
    // peer implementations will throw an exception for other bogus
    // values e.g. -1, 999999
    if (keycode == KeyEvent.VK_UNDEFINED) {
      throw new IllegalArgumentException("Invalid key code");
    }
  }

  /**
   * Returns the color of a pixel at the given screen coordinates.
   *
   * @param x X position of pixel
   * @param y Y position of pixel
   * @return Color of the pixel
   */
  public synchronized Color getPixelColor(int x, int y) {
    Color color = new Color(peer.getRGBPixel(x, y));
    return color;
  }

  /**
   * Creates an image containing pixels read from the screen.  This image does
   * not include the mouse cursor.
   *
   * @param screenRect Rect to capture in screen coordinates
   * @return The captured image
   * @throws IllegalArgumentException if <code>screenRect</code> width and height are not greater
   * than zero
   * @throws SecurityException if <code>readDisplayPixels</code> permission is not granted
   * @see SecurityManager#checkPermission
   * @see AWTPermission
   */
  public synchronized BufferedImage createScreenCapture(Rectangle screenRect) {
    checkScreenCaptureAllowed();

    checkValidRect(screenRect);

    BufferedImage image;
    DataBufferInt buffer;
    WritableRaster raster;

    if (screenCapCM == null) {
            /*
             * Fix for 4285201
             * Create a DirectColorModel equivalent to the default RGB ColorModel,
             * except with no Alpha component.
             */

      screenCapCM = new DirectColorModel(24,
                                               /* red mask */    0x00FF0000,
                                               /* green mask */  0x0000FF00,
                                               /* blue mask */   0x000000FF);
    }

    // need to sync the toolkit prior to grabbing the pixels since in some
    // cases rendering to the screen may be delayed
    Toolkit.getDefaultToolkit().sync();

    int pixels[];
    int[] bandmasks = new int[3];

    pixels = peer.getRGBPixels(screenRect);
    buffer = new DataBufferInt(pixels, pixels.length);

    bandmasks[0] = screenCapCM.getRedMask();
    bandmasks[1] = screenCapCM.getGreenMask();
    bandmasks[2] = screenCapCM.getBlueMask();

    raster = Raster
        .createPackedRaster(buffer, screenRect.width, screenRect.height, screenRect.width,
            bandmasks, null);
    SunWritableRaster.makeTrackable(buffer);

    image = new BufferedImage(screenCapCM, raster, false, null);

    return image;
  }

  private static void checkValidRect(Rectangle rect) {
    if (rect.width <= 0 || rect.height <= 0) {
      throw new IllegalArgumentException("Rectangle width and height must be > 0");
    }
  }

  private static void checkScreenCaptureAllowed() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
      security.checkPermission(
          SecurityConstants.AWT.READ_DISPLAY_PIXELS_PERMISSION);
    }
  }

  /*
   * Called after an event is generated
   */
  private void afterEvent() {
    autoWaitForIdle();
    autoDelay();
  }

  /**
   * Returns whether this Robot automatically invokes <code>waitForIdle</code>
   * after generating an event.
   *
   * @return Whether <code>waitForIdle</code> is automatically called
   */
  public synchronized boolean isAutoWaitForIdle() {
    return isAutoWaitForIdle;
  }

  /**
   * Sets whether this Robot automatically invokes <code>waitForIdle</code>
   * after generating an event.
   *
   * @param isOn Whether <code>waitForIdle</code> is automatically invoked
   */
  public synchronized void setAutoWaitForIdle(boolean isOn) {
    isAutoWaitForIdle = isOn;
  }

  /*
   * Calls waitForIdle after every event if so desired.
   */
  private void autoWaitForIdle() {
    if (isAutoWaitForIdle) {
      waitForIdle();
    }
  }

  /**
   * Returns the number of milliseconds this Robot sleeps after generating an event.
   */
  public synchronized int getAutoDelay() {
    return autoDelay;
  }

  /**
   * Sets the number of milliseconds this Robot sleeps after generating an event.
   *
   * @throws IllegalArgumentException If <code>ms</code> is not between 0 and 60,000 milliseconds
   * inclusive
   */
  public synchronized void setAutoDelay(int ms) {
    checkDelayArgument(ms);
    autoDelay = ms;
  }

  /*
   * Automatically sleeps for the specified interval after event generated.
   */
  private void autoDelay() {
    delay(autoDelay);
  }

  /**
   * Sleeps for the specified time.
   * To catch any <code>InterruptedException</code>s that occur,
   * <code>Thread.sleep()</code> may be used instead.
   *
   * @param ms time to sleep in milliseconds
   * @throws IllegalArgumentException if <code>ms</code> is not between 0 and 60,000 milliseconds
   * inclusive
   * @see java.lang.Thread#sleep
   */
  public synchronized void delay(int ms) {
    checkDelayArgument(ms);
    try {
      Thread.sleep(ms);
    } catch (InterruptedException ite) {
      ite.printStackTrace();
    }
  }

  private void checkDelayArgument(int ms) {
    if (ms < 0 || ms > MAX_DELAY) {
      throw new IllegalArgumentException("Delay must be to 0 to 60,000ms");
    }
  }

  /**
   * Waits until all events currently on the event queue have been processed.
   *
   * @throws IllegalThreadStateException if called on the AWT event dispatching thread
   */
  public synchronized void waitForIdle() {
    checkNotDispatchThread();
    // post a dummy event to the queue so we know when
    // all the events before it have been processed
    try {
      SunToolkit.flushPendingEvents();
      EventQueue.invokeAndWait(new Runnable() {
        public void run() {
          // dummy implementation
        }
      });
    } catch (InterruptedException ite) {
      System.err.println("Robot.waitForIdle, non-fatal exception caught:");
      ite.printStackTrace();
    } catch (InvocationTargetException ine) {
      System.err.println("Robot.waitForIdle, non-fatal exception caught:");
      ine.printStackTrace();
    }
  }

  private void checkNotDispatchThread() {
    if (EventQueue.isDispatchThread()) {
      throw new IllegalThreadStateException("Cannot call method from the event dispatcher thread");
    }
  }

  /**
   * Returns a string representation of this Robot.
   *
   * @return the string representation.
   */
  public synchronized String toString() {
    String params =
        "autoDelay = " + getAutoDelay() + ", " + "autoWaitForIdle = " + isAutoWaitForIdle();
    return getClass().getName() + "[ " + params + " ]";
  }
}
